import * as THREE from 'three';

import * as Utils from '../core/FunctionLibrary';

import CannonDebugger from 'cannon-es-debugger';

import { CameraOperator } from '../core/CameraOperator';
import { GUI } from "../../lib";
import { LoadingManager } from '../core/LoadingManager';
import { Sky } from './Sky';

import { WorldParams } from '../interface/WorldParams';
import { IUpdatable } from '../interface/IUpdatable';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { BoxCollider } from '../physics/colliders/BoxCollider';
import { TrimeshCollider } from '../physics/colliders/TrimeshCollider';
import { CollisionGroups } from '../enums/CollisionGroups';
import { Scenario } from './Scenario';
import { Path } from './Path';
import { InputManager } from '../core/InputManager';
import { Base } from './Base';

import { type Character } from '../characters/Character';
import { IWorldEntity } from '../interface/IWorldEntity';


export class World extends Base {

    public range: THREE.Box3;

    public renderDelta: number;
    public logicDelta: number;
    public requestDelta!: number;
    public sinceLastFrame: number;
    public justRendered: boolean;

    public cannonDebugger: { update: () => void; };
    public scenarios: Scenario[] = [];

    public parallelPairs: any[];

    public physicsFrameRate: number;
    public physicsFrameTime: number;
    public physicsMaxPrediction: number;

    public params: WorldParams;
    public scenarioGUIFolder!: GUI;

    public timeScaleTarget: number = 1;

    public updatables: IUpdatable[] = [];

    public paths: Path[] = [];

    public sky: Sky;
    public cameraOperator: CameraOperator;
    public inputManager: InputManager;

    private lastScenarioID: string;
    characters: Character[];

    constructor ( worldScenePath?: string ) {

        super();


        // debugger
        this.cannonDebugger = CannonDebugger( this.graphicsWorld, this.physicsWorld );

        this.characters = [];
        this.parallelPairs = [];
        this.physicsFrameRate = 60;
        this.physicsFrameTime = 1 / this.physicsFrameRate;
        this.physicsMaxPrediction = this.physicsFrameRate;

        // renderLoop
        this.renderDelta = 0;
        this.logicDelta = 0;
        this.sinceLastFrame = 0;
        this.justRendered = false;


        this.createParamsGUI();
        this.initStats();


        // 初始化
        this.inputManager = new InputManager( this, this.renderer.domElement );
        this.cameraOperator = new CameraOperator( this, this.camera, this.params.Mouse_Sensitivity );
        this.sky = new Sky( this );


        this.range = new THREE.Box3( new THREE.Vector3( -200, 0, 200 ), new THREE.Vector3( 200, 100, 200 ) );


        if ( worldScenePath !== undefined ) {
            const loadingManager = new LoadingManager( this );
            loadingManager.onFinishedCallback = () => {
                console.log( "加载完成了" );
            };

            loadingManager.loadGLTF( worldScenePath, gltf => {
                this.loadScene( loadingManager, gltf );
            } );
        }

        this.render( this );
    }

    /**更新 */
    public update ( timeStep: number, unscaledTimeStep: number ) {

        // 更新物理世界
        this.updatePhysics( timeStep );

        // 更新所有注册的可更新模块
        this.updatables.forEach( updatable => {
            updatable.update( timeStep, unscaledTimeStep );
        } );

        // 
        this.params.Time_Scale = THREE.MathUtils.lerp( this.params.Time_Scale, this.timeScaleTarget, 0.2 );

        if ( this.params.Debug_Physics ) {
            this.cannonDebugger.update();
        }
    }

    public updatePhysics ( timeStep: number ): void {
        // Step the physics world
        this.physicsWorld.step( this.physicsFrameTime, timeStep );


        this.characters.forEach( ( character ) => {
            if ( this.isOutOfBounds( character.characterCapsule.body.position as unknown as THREE.Vector3 ) ) {

                
                // this.outOfBoundsRespawn(character.characterCapsule.body);
            }
        } );

        // this.vehicles.forEach( ( vehicle ) => {
        //     if ( this.isOutOfBounds( vehicle.rayCastVehicle.chassisBody.position ) ) {
        //         let worldPos = new THREE.Vector3();
        //         vehicle.spawnPoint.getWorldPosition( worldPos );
        //         worldPos.y += 1;
        //         this.outOfBoundsRespawn( vehicle.rayCastVehicle.chassisBody, Utils.cannonVector( worldPos ) );
        //     }
        // } );
    }

    public isOutOfBounds ( position: THREE.Vector3 ) {
        return !this.range.containsPoint( position );
    }

    /**注册可更新对象 */
    public registerUpdatable ( register: IUpdatable ): void {
        this.updatables.push( register );
        this.updatables.sort( ( a, b ) => a.updateOrder - b.updateOrder );
    }

    /**注销可更新对象 */
    public unregisterUpdatable ( register: IUpdatable ) {
        const index = this.updatables.indexOf( register );
        this.updatables.splice( index, 1 );
    }

    public loadScene ( loadingManager: LoadingManager, gltf: GLTF ) {
        gltf.scene.traverse( child => {
            if ( child instanceof THREE.Mesh ) {
                Utils.setupMeshProperties( child );
            }

            if ( child.userData.hasOwnProperty( 'data' ) ) {
                if ( child.userData.data === 'physics' ) {
                    if ( child.userData.hasOwnProperty( 'type' ) ) {
                        // Convex doesn't work! Stick to boxes!
                        if ( child.userData.type === 'box' ) {
                            let phys = new BoxCollider( { size: new THREE.Vector3( child.scale.x, child.scale.y, child.scale.z ) } );
                            phys.body.position.copy( Utils.cannonVector( child.position ) );
                            phys.body.quaternion.copy( Utils.cannonQuat( child.quaternion ) );
                            phys.body.updateAABB();

                            phys.body.shapes.forEach( ( shape ) => {
                                shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders;
                            } );

                            this.physicsWorld.addBody( phys.body );
                        } else if ( child.userData.type === 'trimesh' ) {
                            let phys = new TrimeshCollider( child, {} );
                            this.physicsWorld.addBody( phys.body );
                        }

                        child.visible = false;
                    }
                }

                if ( child.userData.data === 'path' ) {
                    this.paths.push( new Path( child ) );
                }

                if ( child.userData.data === 'scenario' ) {
                    this.scenarios.push( new Scenario( child, this ) );
                }
            }
        } );


        this.graphicsWorld.add( gltf.scene );

        let defaultScenarioID: string;
        for ( const scenario of this.scenarios ) {
            if ( scenario.default ) {
                defaultScenarioID = scenario.id;
                break;
            }
        }
        if ( ( defaultScenarioID !== undefined ) ) {

            this.launchScenario( defaultScenarioID, loadingManager );

        }
    }


    public launchScenario ( scenarioID: string, loadingManager?: LoadingManager ): void {

        this.lastScenarioID = scenarioID;

        this.clearEntities();

        if ( !loadingManager ) loadingManager = new LoadingManager( this );

        for ( const scenario of this.scenarios ) {

            if ( scenario.id === scenarioID || scenario.spawnAlways ) {

                scenario.launch( loadingManager, this );

            }

        }

    }

    public clearEntities (): void {

        while ( this.characters.length > 0 ) {
            const character = this.characters.pop();
            this.remove( character );
        }

    }

    public add ( worldEntity: IWorldEntity ): void {
        worldEntity.addToWorld( this );
        this.registerUpdatable( worldEntity );
    }

    public remove ( worldEntity: IWorldEntity ): void {
        worldEntity.removeFromWorld();
        this.unregisterUpdatable( worldEntity );
    }

    /**
     * Rendering loop.
     * Implements fps limiter and frame-skipping
     * Calls world's "update" function before rendering.
     * @param {World} world 
     */
    public render ( world: World ): void {
        this.requestDelta = this.clock.getDelta();

        requestAnimationFrame( () => {
            world.render( world );
        } );

        // Getting timeStep
        let unscaledTimeStep = ( this.requestDelta + this.renderDelta + this.logicDelta );
        let timeStep = unscaledTimeStep * this.params.Time_Scale;
        timeStep = Math.min( timeStep, 1 / 30 );    // min 30 fps

        // Logic
        world.update( timeStep, unscaledTimeStep );

        // Measuring logic time
        this.logicDelta = this.clock.getDelta();

        this.stats.update();

        // Frame limiting
        let interval = 1 / 60;
        this.sinceLastFrame += this.requestDelta + this.renderDelta + this.logicDelta;
        this.sinceLastFrame %= interval;

        if ( this.params.FXAA ) {
            this.composer.render();
        } else {
            this.renderer.render( this.graphicsWorld, this.camera );
        }

        // Measuring render time
        this.renderDelta = this.clock.getDelta();
    }

    setTimeScale ( value: number ) {
        this.params.Time_Scale = value;
        this.timeScaleTarget = value;
    }

    public scrollTheTimeScale ( scrollAmount: number ) {

        const timeScaleBottomLimit = 0.003;
        const timeScaleChangeSpeed = 1.3;

        if ( scrollAmount > 0 ) {
            this.timeScaleTarget /= timeScaleChangeSpeed;
            if ( this.timeScaleTarget < timeScaleBottomLimit ) this.timeScaleTarget = 0;
        } else {
            this.timeScaleTarget *= timeScaleChangeSpeed;

            if ( this.timeScaleTarget < timeScaleBottomLimit ) this.timeScaleTarget = timeScaleBottomLimit;

            this.timeScaleTarget = Math.min( this.timeScaleTarget, 1 );
        }
    }

    public updateControls ( controls: { keys: string[], desc: string; }[] ): void {

        let html = '';
        html += '<h2 class="controls-title">Controls:</h2>';

        controls.forEach( ( row ) => {
            html += '<div class="ctrl-row">';
            row.keys.forEach( ( key ) => {
                if ( key === '+' || key === 'and' || key === 'or' || key === '&' ) html += '&nbsp;' + key + '&nbsp;';
                else html += '<span class="ctrl-key">' + key + '</span>';
            } );

            html += '<span class="ctrl-desc">' + row.desc + '</span></div>';
        } );

        document.getElementById( 'controls' ).innerHTML = html;
    }

    private createParamsGUI (): void {

        const scope = this;
        this.params = {
            Pointer_Lock: true,
            Mouse_Sensitivity: 0.3,
            Time_Scale: 1,
            Shadows: true,
            FXAA: false,
            Debug_Physics: false,
            Debug_FPS: false,
            Sun_Elevation: 50,
            Sun_Rotation: 145,
        };

        // const gui = new GUI();

        // // Scenario
        // this.scenarioGUIFolder = gui.addFolder( 'Scenarios' );
        // this.scenarioGUIFolder.open();

        // // World
        // let worldFolder = gui.addFolder( 'World' );
        // worldFolder.add( this.params, 'Time_Scale', 0, 1 ).listen()
        //     .onChange( ( value:number ) => {
        //         scope.timeScaleTarget = value;
        //     } );


        // // Input
        // let settingsFolder = gui.addFolder( 'Settings' );

        // settingsFolder.add( this.params, 'Debug_Physics' )
        //     .onChange( ( enabled ) => {
        //         if ( enabled ) {
        //             this.cannonDebugRenderer = new CannonDebugRenderer( this.graphicsWorld, this.physicsWorld );
        //         }
        //         else {
        //             this.cannonDebugRenderer.clearMeshes();
        //             this.cannonDebugRenderer = undefined;
        //         }

        //         scope.characters.forEach( ( character ) => {
        //             character.raycastBox.visible = enabled;
        //         } );
        //     } );
        // settingsFolder.add( this.params, 'Debug_FPS' )
        //     .onChange( ( enabled ) => {
        //         UIManager.setFPSVisible( enabled );
        //     } );

        // gui.open();
    }
}